#ifndef THREADSAFE_LINKEDQUEUE_H
#define THREADSAFE_LINKEDQUEUE_H


#include <condition_variable>
#include <memory>
#include <mutex>
#include <QDebug>
#include "interruptible_thread.h"

template<typename T, typename condition_variable, typename = std::enable_if_t<(std::is_same_v<condition_variable, std::condition_variable> || std::is_same_v<condition_variable, std::condition_variable_any>), condition_variable>>
class threadsafe_linkedqueue
{
private:
    struct node
    {
        std::shared_ptr<T> data;
        std::unique_ptr<node> next;
    };
    std::mutex head_mutex;
    std::unique_ptr<node> head;
    std::mutex tail_mutex;
    node* tail;
    condition_variable data_cond;

    node* get_tail()
    {
        std::lock_guard<std::mutex> tail_lock(tail_mutex);
        return tail;
    }

    std::unique_ptr<node> try_pop_head(T& value)
    {
        std::lock_guard<std::mutex> head_lock(head_mutex);
        if(head.get()==get_tail())
        {
            return std::unique_ptr<node>();
        }
        value=std::move(*head->data);
        return pop_head();
    }

    std::unique_ptr<node> pop_head()
    {
        std::unique_ptr<node> old_head=std::move(head);
        head=std::move(old_head->next);
        return old_head;
    }
    std::unique_lock<std::mutex> wait_for_data(int timeMillSeconds)
    {
        std::unique_lock<std::mutex> head_lock(head_mutex);
        if constexpr (std::is_same_v<condition_variable, std::condition_variable>) {
            if(timeMillSeconds > 0) {
                const auto timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeMillSeconds);
                data_cond.wait_until(head_lock, timeout, [&]{
                    return head.get()!=get_tail();
                });
            }
            else {
                data_cond.wait(head_lock,[&]{
                    return head.get()!=get_tail();
                });
            }
        }
        else {
            this_thread_interrupt_flag.wait(data_cond, head_lock, [&]{
                return head.get()!=get_tail();
            }, timeMillSeconds);
        }
        return std::move(head_lock);
    }
    std::unique_ptr<node> wait_pop_head(int timeMillSeconds)
    {
        std::unique_lock<std::mutex> head_lock(wait_for_data(timeMillSeconds));
        if(head.get()==get_tail())
        {
            return std::unique_ptr<node>();
        }
        return pop_head();
    }
    std::unique_ptr<node> wait_pop_head(T& value, int timeMillSeconds)
    {
        std::unique_lock<std::mutex> head_lock(wait_for_data(timeMillSeconds));
        if(head.get()==get_tail())
        {
            return std::unique_ptr<node>();
        }
        value=std::move(*head->data);
        return pop_head();
    }

    std::unique_ptr<node> try_pop_head()
    {
        std::lock_guard<std::mutex> head_lock(head_mutex);
        if(head.get()==get_tail())
        {
            return std::unique_ptr<node>();
        }
        return pop_head();
    }



public:
    threadsafe_linkedqueue():
        head(new node),tail(head.get())
    {}
    threadsafe_linkedqueue(const threadsafe_linkedqueue& other)=delete;
    threadsafe_linkedqueue& operator=(const threadsafe_linkedqueue& other)=delete;

    void clear() {
        {
            std::lock_guard<std::mutex> head_lock(head_mutex);
            head = std::unique_ptr<node>(get_tail());
        }
    }

    std::shared_ptr<T> try_pop()
    {
        std::unique_ptr<node> old_head = try_pop_head();
        return old_head?old_head->data:std::shared_ptr<T>();
    }

    bool try_pop(T& value)
    {
        std::unique_ptr<node> const old_head = try_pop_head(value);
        return old_head ? true : false;
    }

    bool empty()
    {
        std::lock_guard<std::mutex> head_lock(head_mutex);
        return (head.get()==get_tail());
    }

    std::shared_ptr<T> wait_and_pop(int timeMillSeconds = 0)
    {
        std::unique_ptr<node> const old_head=wait_pop_head(timeMillSeconds);
        return old_head ? old_head->data : nullptr;
    }
    bool wait_and_pop(T& value, int timeMillSeconds = 0)
    {
        std::unique_ptr<node> const old_head=wait_pop_head(value, timeMillSeconds);
        return old_head ? true : false;
    }

    void push(T&& new_value){
        std::shared_ptr<T> new_data(std::make_shared<T>(std::forward<T>(new_value)));
        std::unique_ptr<node> p(new node);
        {
            std::lock_guard<std::mutex> tail_lock(tail_mutex);
            tail->data = new_data;
            node *new_tail = p.get();
            tail->next = std::move(p);
            tail = new_tail;
        }
        data_cond.notify_one();
    }

    void push(const T& new_value){
        std::shared_ptr<T> new_data(std::make_shared<T>(new_value));
        std::unique_ptr<node> p(new node);
        {
            std::lock_guard<std::mutex> tail_lock(tail_mutex);
            tail->data = new_data;
            node *new_tail = p.get();
            tail->next = std::move(p);
            tail = new_tail;
        }
        data_cond.notify_one();
    }
};
#endif // THREADSAFE_LINKEDQUEUE_H
